package calao.plan;

import static calao.util.Logger.DEBUG;
import static calao.util.Logger.log;

import java.util.Comparator;
import java.util.HashMap;
import java.util.Vector;

import calao.storage.RangeCondition;
import calao.storage.Record;
import calao.storage.Search;
import calao.storage.Storage;
import calao.util.Cursor;
import calao.util.TimePeriod;
import static calao.util.Logger.*;

/** Plans tasks.
 * This planner does not support tasks marked as started sooner than one week before current
 * system time.
 */
class Plan 
implements Comparator<TaskRef>
{

	public final static long FINALIZE_PERIOD = TimePeriod.WEEK;

	final Storage storage;
	// per-resource schedules
	final HashMap<Integer,Schedule> schedules = new HashMap<Integer,Schedule>();
	// task map, contains uncompleted (scheduled, started and fixed) tasks
	final HashMap<Integer,TaskRef> taskMap = new HashMap<Integer,TaskRef>();
	// task list, to be sorted by (dependency level, priority)
	// contains tasks to plan : scheduled and started tasks
	final Vector<TaskRef> tasks = new Vector<TaskRef>();
	// current time
	final long now = System.currentTimeMillis();
	// start date for auto-scheduled tasks
	final long scheduleStart = now;
	// plan start: fixed tasks ending before are eventually removed from the pool 
	long start = now-FINALIZE_PERIOD;

	Schedule getSchedule(int rid)
	{
		Schedule schedule = schedules.get(rid);
		if (schedule==null) {
			Record record = storage.fetch(ResourceDB.TABLE, rid);
			if (record==null) return null;
			schedule = new Schedule(ResourceDB.INFO_BINDING.toObject(record));
			schedules.put(rid, schedule);
		}
		return schedule;
	}

	Plan(Storage storage)
	{
		this.storage = storage;
		// fetch all tasks to take into account for plan computation
		Search search = new Search(TaskDB.TABLE);
		search.add(new RangeCondition(TaskDB.POOL, SchedulePool.SCHEDULED, SchedulePool.FIXED));
		Cursor<Record> cursor = storage.listResults(search);
		Vector<TimePeriod> busyPeriods = new Vector<TimePeriod>();
		while (cursor.next()) {
			Record rec = cursor.get();
			int resource = rec.getInt(TaskDB.RESOURCE, 0);
			Schedule sched = resource!=0? getSchedule(resource) : null;
			TaskRef ref = new TaskRef(rec, sched);
			if (resource==0) {
				if (ref.pool!=SchedulePool.FIXED) {
					log(ERROR, "Invalid task %d", rec.get(TaskDB.ID));
					continue;					
				}
				busyPeriods.add(new TimePeriod(ref.start, ref.end));
				checkFixed(ref);
			}
			else {
				if (sched==null) {
					log(ERROR, "Cannot schedule task %d: unknown resource %d", 
							rec.get(TaskDB.ID), resource);
					continue;
				}
				taskMap.put(rec.get(TaskDB.ID), ref);
				if (ref.pool==SchedulePool.FIXED) {
					sched.addBusyPeriod(new TimePeriod(ref.start, ref.end));
				}
				else if (sched.isFixed()) {
					// case the schedule does not support floating tasks (i.e. no work period defined)
					unschedule(ref);
				}
				else {
					if (ref.start>0 && ref.start<start) start = ref.start;
					tasks.add(ref);
				}
			}
		}	
		// take shared busy periods into account
		for (Schedule sched : schedules.values()) {
			for (TimePeriod period : busyPeriods) sched.addBusyPeriod(period);
		}
		cursor.close();
	}

	void compute()
	{
		// iterate through the dependency tree to compute dependency level
		// we choose to iterate over the map since this will possibly change tasks list
		for (TaskRef ref : taskMap.values()) {
			if (ref.pool!=SchedulePool.FIXED) computeDepLevel(ref);
		}
		// sort tasks by (dependency level, priority)
		java.util.Collections.sort(tasks, this);
		// schedule each task in order on the planning
		for (TaskRef ref : tasks) {
			long soonestStart = ref.pool==SchedulePool.STARTED? ref.start : scheduleStart;
			for (Integer depId : ref.record.getAll(TaskDB.DEPENDENCIES)) {
				TaskRef dep = taskMap.get(depId);
				if (dep.end>soonestStart) soonestStart = dep.end; 
			}
			int worktime = ref.record.get(TaskDB.WORK_TIME);
			Schedule schedule = getSchedule(ref.doer);
			TimePeriod period = schedule.scheduleWork(worktime, soonestStart);
			if (ref.pool!=SchedulePool.STARTED) ref.start = period.getStartTime();
			ref.end = period.getEndTime();
		}
	}

	void unschedule(TaskRef ref)
	{
		SchedulePool.setPool(ref.record, SchedulePool.UNSCHEDULED);
		tasks.remove(ref);
		storage.store(ref.record);
	}

	private void computeDepLevel(TaskRef ref)
	{
		int max = 0;
		for (int depId : ref.record.getAll(TaskDB.DEPENDENCIES)) {
			TaskRef dep = taskMap.get(depId);
			if (dep==null) {
				// a scheduled or started task that depends on an unplanned task
				// is moved to the unscheduled pool
				unschedule(ref);
			}
			else {
				if (dep.depLevel==0) computeDepLevel(dep);
				if (dep.depLevel>max) max = dep.depLevel;
			}
		}
		ref.depLevel = max + 1;
	}

	public int compare(TaskRef r1, TaskRef r2) 
	{
		if (r1.depLevel<r2.depLevel) return -1;
		if (r1.depLevel>r2.depLevel) return 1;
		if (r1.priority<r2.priority) return -1;
		if (r1.priority>r2.priority) return 1;
		return 0;
	}

	public void saveChanges()
	{
		// TODO think of some cursor-based optimization
		// TODO check for useless store operations (unchanged records) 
		Search search = new Search(TaskDB.TABLE);
		search.add(new RangeCondition(TaskDB.POOL, SchedulePool.SCHEDULED, SchedulePool.FIXED));
		for (TaskRef ref : taskMap.values()) {
			if (ref.pool==SchedulePool.FIXED) checkFixed(ref);
			else {
				boolean mod = TaskDB.setStart(ref.record, ref.start);
				mod |= TaskDB.setEnd(ref.record, ref.end);
				if (mod) {
					storage.store(ref.record);
					log(DEBUG, "Stored task %s", ref.record);
				}
			}
		}	
	}

	public void checkFixed(TaskRef ref) {
		if (ref.end<start) {
			// remove fixed tasks ending before plan start
			TaskDB.removePool(ref.record, SchedulePool.FIXED);
			TaskDB.setFlag(ref.record, TaskDB.FINALIZED_FLAG);
			storage.store(ref.record);
			log(DEBUG, "Stored task %s", ref.record);
		}		
	}
	
}
